使用 JShell API 实现 Java 源代码浏览器

除了作为命令行 REPL 工具之外,jshell 还提供了一个编程 API

从 JDK 19 开始,jshell 会高亮显示 Java 代码段中的关键字、标识符和已弃用 API。这个新特性也通过jshell API 公开。

下面的示例展示了如何使用 JShell API 和 JDK 18 中引入的简单 Web 服务器 API 来实现一个简单的 Java 源代码浏览器。由于它是作为Java shebang 脚本实现的,因此可以直接调用。您只需要传递一个包含一些 Java 源代码的目录。

./srcbrowser ../../jdk/open/src



并在浏览器中打开 https://127.0.0.1:8080 来浏览源代码。



来源

#!/usr/bin/java -source 19

/*
 * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   - Neither the name of Oracle nor the names of its
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.io.*;
import java.nio.file.*;
import java.net.*;
import java.util.*;
import java.util.stream.*;
import java.util.function.Predicate;
import java.util.spi.ToolProvider;
import jdk.jshell.*;
import com.sun.net.httpserver.*;
import static com.sun.net.httpserver.SimpleFileServer.*;

/**
 * Serves the default file system via webserver.
 * Decorates .java source files as HTML using JShell APIs.
 *
 * ./srcbrowser [<base dir>] [<port>]
 */
class srcbrowser {
  // base path to serve
  private static Path BASE_DIR;

  public static void main(String[] args) throws Exception {
    BASE_DIR = args.length > 0? Paths.get(args[0]) : Paths.get(".");
    BASE_DIR = BASE_DIR.toAbsolutePath();
    var fileHandler = SimpleFileServer.createFileHandler(BASE_DIR);

    // if it's a java source file, then send HTML generated using jshell
    // else use the default (file system) handler
    var handler = HttpHandlers.handleOrElse(
      srcbrowser::isJavaSource, srcbrowser::sendHtmlForJava, fileHandler);
    var output = SimpleFileServer.createOutputFilter(
      System.out, OutputLevel.VERBOSE);

    var port = args.length > 1? Integer.parseInt(args[1]) : 8080;
    var lookback = new InetSocketAddress(port);
    var server = HttpServer.create(lookback, 10, "/", handler, output);
    System.out.printf("visit https://127.0.0.1:%d/ from your browser..", port);
    server.start();
  }

  // is this a .java source file
  private static boolean isJavaSource(Request r) {
    return r.getRequestURI().toString().endsWith(".java");
  }

  private static void sendHtmlForJava(HttpExchange exchange) {
    try {
      var path = BASE_DIR.resolve(
        // get rid of initial '/' from the URI
        exchange.getRequestURI().toString().substring(1));
      if (!Files.exists(path)) {
        exchange.sendResponseHeaders(404, -1);
        exchange.close();
      } else {
        exchange.sendResponseHeaders(200, 0);
        var out = new PrintStream(exchange.getResponseBody(), true);
        var src = Files.readString(path);
        out.println(srcToHTML(src));
        exchange.close();
      }
    } catch (IOException exp) {
      exp.printStackTrace();
    }
  }

  private static String srcToHTML(String src) {
    var jshell = JShell.builder().executionEngine("local").build();
    var srcAnalysis = jshell.sourceCodeAnalysis();
    var buf = new StringBuffer();
    buf.append("<html><body><pre><code>");
    int index = 0;
    for (var hl : srcAnalysis.highlights(src)) {
      if (index < hl.start()) {
        buf.append(htmlEncode(src.substring(index, hl.start())));
      }
      buf.append(decorate(hl.attributes(), 
          src.substring(hl.start(), hl.end())));
      index = hl.end();
    }
    buf.append(htmlEncode(src.substring(index)));
    buf.append("</pre></code></body></htm>");
    return buf.toString();
  }

  private static String decorate(Set<SourceCodeAnalysis.Attribute> attrs, String content) {
    var buf = new StringBuilder();
    // start tags
    buf.append(
      attrs.stream().map(attr -> switch(attr) {
          case DECLARATION -> "<b>";
          case DEPRECATED -> "<s>";
          case KEYWORD -> "<font color=\"red\">";
      }).collect(Collectors.joining()));
    // content inside
    buf.append(htmlEncode(content));
    // end tags
    buf.append(
      attrs.stream().map(attr -> switch(attr) {
          case DECLARATION -> "</b>";
          case DEPRECATED -> "</s>";
          case KEYWORD -> "</font>";
      }).collect(Collectors.joining()));
    return buf.toString();
  }

  private static String htmlEncode(String src) {
    var buf = new StringBuilder();
    for (char c : src.toCharArray()) {
       switch (c) {
          case '<' -> buf.append("&lt;");
          case '>' -> buf.append("&gt;");
          case '&' -> buf.append("&amp;");
          case '"' -> buf.append("&quot;");
          default -> buf.append(c);
       }
     }
     return buf.toString();
  }
}